/**
 * Engine implementation
 */

#include <math.h>

#include "Engine.h"
#include "Shader.h"
#include "FloatTexture.h"
#include "ByteTexture.h"
#include "DepthTexture.h"

#include "shared.h"

/////////////////////////////////////////////////

Engine::Engine() : _size(0,0), _numSecLights(NUM_INDIRECT_LIGHTS), _numNegLights(NUM_INDIRECT_LIGHTS/2), _radOn(RADIOSITY), 
				   _showFromLight(false), _displayTex(2), _showSecLights(false),
				   _showDS(false), _secLightsFromCamera(false), _useHemisphereSampling(true),
                   _useNegativeIndirectLight(NEGATIVE_LIGHT)
{
}

/////////////////////////////////////////////////

void Engine::init(const Vec2s& size)
{
	_size = size;
	std::cerr << "Engine init." << std::endl;
	glClearColor(0.0, 0.0, 0.0, 1.0);
	glDepthFunc(GL_LEQUAL);
	glCullFace(GL_BACK);
	glEnable(GL_CULL_FACE);
	glPolygonOffset(3,2);
	
	// Load shaders
	std::cerr << "Loading shaders." << std::endl;
	// Deferred shaders
	ProgramPtr dsProgram(new Program("deferred shader ec", "./src/ds.vert", "./src/ds.frag"));
	_dsProg = dsProgram;
	_dsProg->load();
	_dsProg->setVec2("screenSize", Vec2f(_size[0], _size[1]));
	_dsProg->unload();
	
	ProgramPtr avgProgram(new Program("phong", "./src/average.vert", "./src/average.frag"));
	_avgDownProg = avgProgram;
	
	ProgramPtr pProgram(new Program("phong", "./src/phong.vert", "./src/phong.frag"));
	_defaultProgram = pProgram;
	_defaultProgram->load();
	_defaultProgram->setSampler("dsEC", 0);
	_defaultProgram->setSampler("dsNorm", 1);
	_defaultProgram->setSampler("dsColor", 5);
	_defaultProgram->unload();

	ProgramPtr sProgram(new Program("sec light phong", "./src/secPhong.vert", "./src/secPhong.frag"));
	_secLightProgram = sProgram;
	_secLightProgram->load();
	_secLightProgram->setSampler("dsEC", 0);
	_secLightProgram->setSampler("dsNorm", 1);
	_secLightProgram->setSampler("dsColor", 5);
	_secLightProgram->setFloat("spotCosCutoff", 0.0);
	_secLightProgram->setFloat("cAtt", 1.0);
	_secLightProgram->setFloat("lAtt", 1.0);
	_secLightProgram->setFloat("qAtt", 0.0);
	_secLightProgram->unload();

	ProgramPtr nProgram(new Program("neg light phong", "./src/secPhong.vert", "./src/negPhong.frag"));
	_negLightProgram = nProgram;
	_negLightProgram->load();
	_negLightProgram->setSampler("dsEC", 0);
	_negLightProgram->setSampler("dsNorm", 1);
	_negLightProgram->setSampler("dsColor", 5);
	_negLightProgram->setFloat("spotCosCutoff", 0.0);
	_negLightProgram->setFloat("cAtt", 1.0);
	_negLightProgram->setFloat("lAtt", 1.0);
	_negLightProgram->setFloat("qAtt", 0.0);
	_negLightProgram->unload();

	ProgramPtr ssProgram(new Program("sec light phong", "./src/secPhong.vert", "./src/showSecLights.frag"));
	_showSecLightsProgram = ssProgram;
	_showSecLightsProgram->load();
	_showSecLightsProgram->setSampler("dsEC", 0);
	_showSecLightsProgram->setFloat("lightSize", 0.125);
	_showSecLightsProgram->unload();
	
	ProgramPtr disProgram(new Program("dis", "./src/dis.vert", "./src/dis.frag"));
	ProgramPtr dsDisProgram(new Program("dsdis", "./src/dis.vert", "./src/dsdis.frag"));
	_disProgram = disProgram;
	_dsDisProgram = dsDisProgram;

	// From light shaders
	ProgramPtr fromLightProgram(new Program("from light", "./src/fromLight.vert", "./src/fromLight.frag"));
	_fromLightProg = fromLightProgram;
	std::cerr << "Shaders loaded." << std::endl;

	// Create render textures and framebuffers 
	std::cerr << "Creating render targets." << std::endl;

	FramebufferPtr fboDis(new Framebuffer("framebuffer dis buffer"));
	FramebufferPtr fboFromLight(new Framebuffer("framebuffer object from light"));
	FramebufferPtr fboIndirect(new Framebuffer("framebuffer object indirect"));
	FramebufferPtr fboNegIndirect(new Framebuffer("framebuffer object neg indirect"));
	FramebufferPtr fboUpSample(new Framebuffer("framebuffer object used for upsampling after lowres indirect pass"));
    RenderbufferPtr rb(new Renderbuffer("depth buffer", _size,
                                        GL_DEPTH_ATTACHMENT_EXT, GL_DEPTH_COMPONENT));
	_fboDis = fboDis;
	_fboFromLight = fboFromLight;
	_fboIndirect = fboIndirect;
	_fboNegIndirect = fboNegIndirect;
	_fboUpSample = fboUpSample;
#if 1
	TexturePtr texNorm(new FloatTexture("normal texture", _size));
	TexturePtr texEC(new FloatTexture("ec texture", _size));
	TexturePtr texColor(new FloatTexture("color texture", _size));
#else
	TexturePtr texNorm(new ByteTexture("normal texture", _size));
	TexturePtr texEC(new ByteTexture("ec texture", _size));
	TexturePtr texColor(new ByteTexture("color texture", _size));
#endif
	TexturePtr texDepth(new DepthTexture("depth texture", _size));

	_fromLight[0] = texEC;
	_fromLight[1] = texNorm;
	_fromLight[2] = texColor;
	_fromLight[3] = texDepth;

	TexturePtr indirectLightsEC(new FloatTexture("indirect light pos", Vec2s(INDIRECT_LIGHTS_WIDTH, INDIRECT_LIGHTS_HEIGHT)));
	TexturePtr indirectLightsNorm(new FloatTexture("indirect light norm", Vec2s(INDIRECT_LIGHTS_WIDTH, INDIRECT_LIGHTS_HEIGHT)));
	TexturePtr indirectLightsColor(new FloatTexture("indirect light color", Vec2s(INDIRECT_LIGHTS_WIDTH, INDIRECT_LIGHTS_HEIGHT)));
	TexturePtr indirectLightSamples(new FloatTexture("indirect light samples", Vec2s(INDIRECT_LIGHTS_WIDTH, INDIRECT_LIGHTS_HEIGHT)));
	_indirectLights[0] = indirectLightsEC;
	_indirectLights[1] = indirectLightsNorm;
	_indirectLights[2] = indirectLightsColor;
	_indirectLightSamples = indirectLightSamples;
	
	FramebufferPtr fboIndirectLights(new Framebuffer("framebuffer object indirect lights"));
	_fboIndirectLights = fboIndirectLights;
	_fboIndirectLights->bind();
	_fboIndirectLights->attachTexture(_indirectLights[0], GL_COLOR_ATTACHMENT0_EXT);
	_fboIndirectLights->attachTexture(_indirectLights[1], GL_COLOR_ATTACHMENT1_EXT);
	_fboIndirectLights->attachTexture(_indirectLights[2], GL_COLOR_ATTACHMENT2_EXT);
	_fboIndirectLights->unbind();

	ProgramPtr gatherProg(new Program("gather indirect lights", "./src/gatherIndirectLights.vert", "./src/gatherIndirectLights.frag"));
	_gatherIndirectProg = gatherProg;

	Vec2s dsSize = _size;
	for (int i=0; i<=LO_RES_INDEX; i++) {
		FramebufferPtr fboDS(new Framebuffer("framebuffer object ds"));
		_fboDS[i] = fboDS;
		TexturePtr ds0(new FloatTexture("deferred shader results ec", dsSize));
		TexturePtr ds1(new FloatTexture("deferred shader results norm", dsSize));
		TexturePtr ds2(new FloatTexture("deferred shader results color", dsSize));
		_ds[i][0] = ds0;
		_ds[i][1] = ds1;
		_ds[i][2] = ds2;
		
		_fboDS[i]->bind();
		if (i==0) _fboDS[i]->attachRenderbuffer(rb);
		_fboDS[i]->attachTexture(_ds[i][0], GL_COLOR_ATTACHMENT0_EXT);
		_fboDS[i]->attachTexture(_ds[i][1], GL_COLOR_ATTACHMENT1_EXT);
		_fboDS[i]->attachTexture(_ds[i][2], GL_COLOR_ATTACHMENT2_EXT);
		_fboDS[i]->unbind();

		TexturePtr f0(new FloatTexture(std::string("indirect0"), dsSize));
		TexturePtr f1(new FloatTexture(std::string("indirect1"), dsSize));
		TexturePtr negi0(new FloatTexture(std::string("negindirect0"), dsSize));
		TexturePtr negi1(new FloatTexture(std::string("negindirect1"), dsSize));
		TexturePtr negnormi0(new FloatTexture(std::string("negindirectnorm0"), dsSize));
		TexturePtr negnormi1(new FloatTexture(std::string("negindirectnorm1"), dsSize));
		_indirect[i][0][0] = f0;
		_indirect[i][0][1] = negi0;
		_indirect[i][0][2] = negnormi0;
		_indirect[i][1][0] = f1;
		_indirect[i][1][1] = negi1;
		_indirect[i][1][2] = negnormi1;

		TexturePtr finalneg0(new FloatTexture(std::string("final neg indirect0"), dsSize));
		TexturePtr finalneg1(new FloatTexture(std::string("final neg indirect1"), dsSize));
        _negIndirect[i][0] = finalneg0;
        _negIndirect[i][1] = finalneg1;

		dsSize = dsSize/2;
	}

	dsSize = _size;
	for (int i=0; i<=DIS_INDEX; i++) {
		// TODO just use one component
		TexturePtr disTex(new ByteTexture("dis buffer", dsSize));
		_disBuffer[i] = disTex;
		dsSize = dsSize/2;
	}

	_fboFromLight->bind();
	_fboFromLight->attachTexture(_fromLight[0], GL_COLOR_ATTACHMENT0_EXT);
	_fboFromLight->attachTexture(_fromLight[1], GL_COLOR_ATTACHMENT1_EXT);
	_fboFromLight->attachTexture(_fromLight[2], GL_COLOR_ATTACHMENT2_EXT);
	_fboFromLight->attachTexture(_fromLight[3], GL_DEPTH_ATTACHMENT_EXT);
	_fboFromLight->unbind();
	
	std::cerr << "Render targets created." << std::endl;

	_indirectSamples.reserve(_numSecLights);
    _secLightIndexes = new Vec2f[_numSecLights];
    //standardSamples();
	checkGLError();
}

/////////////////////////////////////////////////

void Engine::resize(const Vec2s& size)
{
	GLuint w = size[0];
	GLuint h = size[1];
	glViewport(0,0,w,h);
	_size = size;
}

/////////////////////////////////////////////////

void Engine::standardSamples()
{
	//assert(0);
	for (unsigned int j=0; j<_numSecLights; ++j) {
		//_secLightIndexes[j] = Vec2f(phiHalton(2, j)*_size[0], phiHalton(3, j)*_size[1]);
		_secLightIndexes[j] = Vec2f( phiHalton(2, j)*(_BBmax[0] - _BBmin[0]) + _BBmin[0], 
                                     phiHalton(3, j)*(_BBmax[1] - _BBmin[1]) + _BBmin[1] );
	}
}

/////////////////////////////////////////////////

void Engine::hemisphereSamples(const Camera& lightCam)
{
	float cutoff = 0.5*lightCam.getFovy();
	Vec4 ec; // Halton point in camera space
	Vec4 ic; // Halton point in image space
	Vec3 dir(0.0, 0.0, -1.0);
	float dot = cos(osg::DegreesToRadians(cutoff));
	float samples[NUM_INDIRECT_LIGHTS*3];
	for (unsigned int j=0,i=0; j<_numSecLights; ++j) {
		GLfloat a, e;
		do {
			++i;
			a = 2.0 * M_PI * phiHalton(2, i);
			e = asinf(2*phiHalton(3, i) - 1);
			ec[0] = sin(a)*cos(e);
			ec[1] = cos(a) * cos(e);
			ec[2] = -sin(e);
		} while (!(ec[2]<0 && (dir*ec)>=dot)); // While not facing the right way and outside than the light fov

		// Convert to image space...
		ic = ec * lightCam.getProjMatrix();
		ic /= ic[3];
		ic *= 0.5;
		ic += Vec4(0.5, 0.5, 0.0, 0.0);
		ic[0] *= _size[0];
		ic[1] *= _size[1];
		_indirectSamples[j] = Vec2s((GLuint)ic[0], (GLuint)ic[1]);
		samples[j*3] = ic[0];
		samples[j*3+1] = ic[1];
		samples[j*3+2] = 0;
		//Vec2s texIndex(j%INDIRECT_LIGHTS_WIDTH, j/INDIRECT_LIGHTS_WIDTH);
		//std::cout << j << " : " << texIndex << std::endl;
	}

	_indirectLightSamples->bind();
	glTexImage2D(_indirectLightSamples->getTarget(), 0, _indirectLightSamples->getInternalFormat(), 
			INDIRECT_LIGHTS_WIDTH, INDIRECT_LIGHTS_HEIGHT, 0, GL_RGB, GL_FLOAT, samples);

}

/////////////////////////////////////////////////

void Engine::render() 
{
	Camera realCam(Vec3(0.0, 2.73, 2.50), 
                   Vec3(0.0, 0.0, -1.0), 
                   Vec3(0.0, 1.0, 0.0), 55.0, 0.1, 20.0, _size);

	LightPtr light = _scene->getLight(0);
	Camera lightCam = Camera(light->getCenter(), light->getDir(), light->getUp(), 2*light->getFov(), 0.1, 20.0, _size);

	if (_useHemisphereSampling) {
		hemisphereSamples(lightCam);
	} else {
		standardSamples();
	}

	if (_showFromLight) {
		glEnable(GL_DEPTH_TEST);
		renderFromLight(light, lightCam, realCam);
		checkGLError();
		_fromLight[_displayTex]->drawFullScreen();
		// Overlay the halton points
		glDisable(GL_DEPTH_TEST);
		glPointSize(5.0);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		gluOrtho2D(0.0, _size[0], 0.0, _size[1]);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glColor3f(1.0, 1.0, 0.0);
		glBegin(GL_POINTS);
		for (unsigned int j=0; j<_numSecLights; ++j) {
			glVertex3f(_indirectSamples[j][0], _indirectSamples[j][1], 0.5);
		}
		glEnd();
	} else if (_showDS) {
		glEnable(GL_DEPTH_TEST);
		renderDS(light, lightCam, realCam);
		//_ds[3][_displayTex]->drawFullScreen();
		_disBuffer[_displayTex ? DIS_INDEX : 0]->drawFullScreen();
		//_indirectLights[_displayTex]->drawFullScreen();
		//_indirectLightSamples->drawFullScreen();
	} else {
		// Allows measuring the final pass speed
		// by only computing the first passes once
		static bool firstTime = true;
		if (firstTime) {
			//firstTime = false;
			// First, render from the light to grab some information...
			glEnable(GL_DEPTH_TEST);
			renderFromLight(light, lightCam, realCam);
			checkGLError();
			// Next, create the deferred shaders textures
			renderDS(light, lightCam, realCam);
			checkGLError();
		}
		// Final, combine the light contributions
		glDisable(GL_DEPTH_TEST);
		renderFinal(realCam);
		checkGLError();
	}
}
/////////////////////////////////////////////////

void Engine::renderFromLight(LightPtr l, const Camera& lightCam, const Camera& realCam)
{
	glEnable(GL_POLYGON_OFFSET_FILL);
	_fromLightProg->load();
	_fboFromLight->bind();
	Matrix m = Matrix::inverse(lightCam.getViewMatrix()) * realCam.getViewMatrix();
	_fromLightProg->setMatrix("toCameraEC", m);
	_fboFromLight->drawColorAttachments(3);
	glClear(GL_DEPTH_BUFFER_BIT);
	drawEntities(l, lightCam);
	_fboFromLight->unbind();
	_fromLightProg->unload();
	glDisable(GL_POLYGON_OFFSET_FILL);

	// Gather the indirect lights into the indirect light textures
	_gatherIndirectProg->load();
	_fboIndirectLights->bind();
	_fboIndirectLights->drawColorAttachments(3);
	_gatherIndirectProg->setSampler("fromLightEC", 0);
	_gatherIndirectProg->setSampler("fromLightNorm", 1);
	_gatherIndirectProg->setSampler("fromLightColor", 2);
	_gatherIndirectProg->setSampler("fromLightIndexes", 3);
	_fromLight[0]->bind(GL_TEXTURE0);
	_fromLight[1]->bind(GL_TEXTURE1);
	_fromLight[2]->bind(GL_TEXTURE2);
	_indirectLightSamples->bind(GL_TEXTURE3);

	drawQuad(0,0,INDIRECT_LIGHTS_WIDTH, INDIRECT_LIGHTS_HEIGHT);

	_fboIndirectLights->unbind();
	_gatherIndirectProg->unload();
}

/////////////////////////////////////////////////

void Engine::renderDS(LightPtr l, const Camera& lightCam, const Camera& c)
{
	Matrix light_view_mat = lightCam.getViewMatrix();
	Matrix light_proj_mat = lightCam.getProjMatrix();
	Matrix texture_mat = Matrix::inverse(c.getViewMatrix()) * light_view_mat * light_proj_mat * Matrix::scale(0.5, 0.5, 0.5) * Matrix::translate( 0.5, 0.5, 0.5 );

	// shadow map
	_fromLight[3]->bind(GL_TEXTURE0);
	_dsProg->load();
	_dsProg->setMatrix( "shadowMat", texture_mat );
	_dsProg->setSampler( "shadowMap", 0 );
	_fboDS[0]->bind();
	_fboDS[0]->drawColorAttachments(3);
	glClear(GL_DEPTH_BUFFER_BIT);
	drawEntities(l, c);
	_fboDS[0]->unbind();
	_dsProg->unload();

	// Down sample the ds
#if 1
	Vec2s size = _size/2;
	_avgDownProg->load();
	_avgDownProg->setSampler("src0", 0);
	_avgDownProg->setSampler("src1", 1);
	_avgDownProg->setSampler("src2", 2);
	_avgDownProg->setFloat("reduction", 2);
	for (int i=1; i<=LO_RES_INDEX; i++) {
		_fboDS[i]->bind();
		_fboDS[i]->drawColorAttachments(3);
		_ds[i-1][0]->bind(GL_TEXTURE0);
		_ds[i-1][1]->bind(GL_TEXTURE1);
		_ds[i-1][2]->bind(GL_TEXTURE2);
		drawQuad(0,0,size[0],size[1]);
		size = size / 2;
		_fboDS[i]->unbind();
	}
	_avgDownProg->unload();

	// TODO do we need both steps?
	// Compute dis buffer
	
	_disProgram->load();
	_fboDis->bind();
	_fboDis->attachTexture(_disBuffer[0], GL_COLOR_ATTACHMENT0_EXT);
	_fboDis->drawColorAttachments(1);
	_disProgram->setSampler("dsECHi", 0);
	_disProgram->setSampler("dsNormHi", 1);
	_disProgram->setSampler("dsECLo", 2);
	_disProgram->setSampler("dsNormLo", 3);
	_ds[0][0]->bind(GL_TEXTURE0);
	_ds[0][1]->bind(GL_TEXTURE1);
	_ds[LO_RES_INDEX][0]->bind(GL_TEXTURE2);
	_ds[LO_RES_INDEX][1]->bind(GL_TEXTURE3);
	drawQuad(0,0,_size[0],_size[1]);
	_fboDis->unbind();
	_disProgram->unload();

	// Downsample dis buffer
	size = _size/2;
	_dsDisProgram->load();
	_fboDis->bind();

	for (int i=1; i<=DIS_INDEX; i++) {
		_fboDis->attachTexture(_disBuffer[i], GL_COLOR_ATTACHMENT0_EXT);
		_fboDis->drawColorAttachments(1);
		_dsDisProgram->setSampler("src0", 0);
		_disBuffer[i-1]->bind(GL_TEXTURE0);
		drawQuad(0,0, size[0], size[1]);
		size = size/2;
	}
	
	_fboDis->unbind();
	_dsDisProgram->unload();
	
	_fboDis->bind();
	_fboDis->attachTexture(_disBuffer[DIS_INDEX], GL_COLOR_ATTACHMENT0_EXT);
	glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
	_disBuffer[DIS_INDEX]->readPixels();
	_fboDis->unbind();
#endif

}

/////////////////////////////////////////////////

int Engine::renderNegIndirect(const Camera& cam, TexturePtr ds[3], 
                              TexturePtr negIndirect[2], 
                              TexturePtr indirectLights[3], 
                              int offset, int pingpong, GLfloat prevScale, GLfloat scale=1.0)
{
	_negLightProgram->load();
	_fboNegIndirect->bind();
	_fboNegIndirect->drawColorAttachments(1);
		
	ds[0]->bind(GL_TEXTURE0);
	ds[1]->bind(GL_TEXTURE1);
	ds[2]->bind(GL_TEXTURE5);

	_negLightProgram->setSampler("lightPos", 10);
	_negLightProgram->setSampler("lightDir", 11);
	_negLightProgram->setSampler("lightColor", 12);
    _ds[0][0]->bind(GL_TEXTURE10);
	indirectLights[2]->bind(GL_TEXTURE11);
	indirectLights[1]->bind(GL_TEXTURE12);

	_negLightProgram->setSampler("prevIndirect", 6); 
	_negLightProgram->setFloat("lightScale", scale*10.0/_numSecLights);
	_negLightProgram->setFloat("prevScale", prevScale);
	//_negLightProgram->setFloat("lightOffset", offset);

	_fboNegIndirect->attachTexture(negIndirect[pingpong], GL_COLOR_ATTACHMENT0_EXT);

	negIndirect[!pingpong]->bind(GL_TEXTURE6);
	//glClear(GL_COLOR_BUFFER_BIT);
	_negLightProgram->setVec2Array("lightIndex", LIGHTS_PER_PASS, _secLightIndexes+offset);

	drawQuad(0,0, ds[0]->getSize()[0], ds[0]->getSize()[1]);

	_fboNegIndirect->unbind();
	_negLightProgram->unload();

	return !pingpong;
}


/////////////////////////////////////////////////

int Engine::renderIndirect(const Camera& cam, TexturePtr ds[3], 
                           TexturePtr indirect[2][3], 
                           int offset, int pingpong, GLfloat prevScale, GLfloat scale=1)
{
	// Now, add in the secondary lights
	_secLightProgram->load();
	_fboIndirect->bind();
	_fboIndirect->drawColorAttachments(3);
	
	ds[0]->bind(GL_TEXTURE0);
	ds[1]->bind(GL_TEXTURE1);
	ds[2]->bind(GL_TEXTURE5);

	_secLightProgram->setSampler("prevImage", 6); 
	_secLightProgram->setSampler("prevNegImage", 7); 
	_secLightProgram->setSampler("prevNegNormals", 8); 
	_secLightProgram->setFloat("lightScale", scale*10.0/_numSecLights);
	_secLightProgram->setFloat("prevScale", prevScale);
	_secLightProgram->setFloat("lightOffset", offset);

	_fboIndirect->attachTexture(indirect[pingpong][0], GL_COLOR_ATTACHMENT0_EXT);
	_fboIndirect->attachTexture(indirect[pingpong][1], GL_COLOR_ATTACHMENT1_EXT);
	_fboIndirect->attachTexture(indirect[pingpong][2], GL_COLOR_ATTACHMENT2_EXT);

	indirect[!pingpong][0]->bind(GL_TEXTURE6);
	indirect[!pingpong][1]->bind(GL_TEXTURE7);
	indirect[!pingpong][2]->bind(GL_TEXTURE8);
	//glClear(GL_COLOR_BUFFER_BIT);
	/*
	_secLightProgram->setVec3Array("lightPos", LIGHTS_PER_PASS, lightPos);
	_secLightProgram->setVec3Array("lightDir", LIGHTS_PER_PASS, lightDir);
	_secLightProgram->setVec4Array("lightColor", LIGHTS_PER_PASS, lightCol);
	*/
	_secLightProgram->setSampler("lightPos", 10);
	_secLightProgram->setSampler("lightDir", 11);
	_secLightProgram->setSampler("lightColor", 12);
	_indirectLights[0]->bind(GL_TEXTURE10);
	_indirectLights[1]->bind(GL_TEXTURE11);
	_indirectLights[2]->bind(GL_TEXTURE12);

	drawQuad(0,0, ds[0]->getSize()[0], ds[0]->getSize()[1]);

	_fboIndirect->unbind();
	_secLightProgram->unload();

	return !pingpong;
}

/////////////////////////////////////////////////

int Engine::renderHiNegIndirect(const Camera& cam, TexturePtr ds[3], 
                                TexturePtr negIndirect[2], 
                                TexturePtr indirectLights[3], 
                                int offset, int pingpong, GLfloat prevScale,
                                GLfloat scale=1.0)
{
	_negLightProgram->load();
	_fboNegIndirect->bind();
	_fboNegIndirect->drawColorAttachments(1);
		
	ds[0]->bind(GL_TEXTURE0);
	ds[1]->bind(GL_TEXTURE1);
	ds[2]->bind(GL_TEXTURE5);

	_negLightProgram->setSampler("lightPos", 10);
	_negLightProgram->setSampler("lightDir", 11);
	_negLightProgram->setSampler("lightColor", 12);
    _ds[0][0]->bind(GL_TEXTURE10);
	indirectLights[2]->bind(GL_TEXTURE11);
	indirectLights[1]->bind(GL_TEXTURE12);

	_negLightProgram->setSampler("prevIndirect", 6); 
	_negLightProgram->setFloat("lightScale", scale*10.0/_numSecLights);
	_negLightProgram->setFloat("prevScale", prevScale);
	//_negLightProgram->setFloat("lightOffset", offset);

	_fboNegIndirect->attachTexture(negIndirect[pingpong], GL_COLOR_ATTACHMENT0_EXT);

	negIndirect[!pingpong]->bind(GL_TEXTURE6);
	_negLightProgram->setVec2Array("lightIndex", LIGHTS_PER_PASS, _secLightIndexes+offset);

	Vec4 c;
	GLuint startX=0, startY=0;
	GLuint width=0, height=0;
	bool started = false;
	int count =0 ;
	for (GLuint x=0; x<_size[0]/DIS_REDUCTION; x++) {
		for (GLuint y=0; y<_size[1]/DIS_REDUCTION; y++) {
			c = _disBuffer[DIS_INDEX]->getPixel(Vec2s(x,y));
			// Try to group vertical (or horizontal depending on the loop above) pixels to form one quad
			if (c[0] > 0.0 && !started) {
				started = true;
				/*
				startX = (x+1)*DIS_REDUCTION;
				startY = (y+1)*DIS_REDUCTION;
				width = DIS_REDUCTION;
				height = DIS_REDUCTION;
				*/

				startX = ((x == 0) ? x : (x+1))*DIS_REDUCTION;// + ((x == 0) ? 0 : DIS_REDUCTION/2);
				startY = ((y == 0) ? y : (y+1))*DIS_REDUCTION;// + ((y == 0) ? 0 : DIS_REDUCTION/2);
				width = ((x == 0) ? 2 : 1)*DIS_REDUCTION;// + ((x == 0) ? DIS_REDUCTION/2 : 0);
				height = ((y == 0) ? 2 : 1)*DIS_REDUCTION;// + ((y == 0) ? DIS_REDUCTION/2 : 0);
			} else {
				height += DIS_REDUCTION;
			}
			if ((c[0] == 0.0 && started) || (started && y+1 == _size[1]/DIS_REDUCTION)) {
				if (c[0] != 0) height += DIS_REDUCTION;
				started = false;
				drawQuad(startX, startY, width, height);
				count++;
			}
		}
	}
	//std::cerr << count << std::endl;

	_fboNegIndirect->unbind();
	_negLightProgram->unload();

	return !pingpong;

}

/////////////////////////////////////////////////

int Engine::renderHiIndirect(const Camera& cam, TexturePtr ds[3], 
                             TexturePtr indirect[2][3], 
                             int offset, int pingpong, GLfloat prevScale)
{

	_secLightProgram->load();
	_fboIndirect->bind();
	_fboIndirect->drawColorAttachments(3);
	
	ds[0]->bind(GL_TEXTURE0);
	ds[1]->bind(GL_TEXTURE1);
	ds[2]->bind(GL_TEXTURE5);

	_secLightProgram->setSampler("prevImage", 6); 
	_secLightProgram->setSampler("prevNegImage", 7); 
	_secLightProgram->setSampler("prevNegNormals", 8); 
	_secLightProgram->setFloat("lightScale", 10.0/_numSecLights);
	_secLightProgram->setFloat("prevScale", prevScale);
	_secLightProgram->setFloat("lightOffset", offset);

	_fboIndirect->attachTexture(indirect[pingpong][0], GL_COLOR_ATTACHMENT0_EXT);
	_fboIndirect->attachTexture(indirect[pingpong][1], GL_COLOR_ATTACHMENT1_EXT);
	_fboIndirect->attachTexture(indirect[pingpong][2], GL_COLOR_ATTACHMENT2_EXT);

	indirect[!pingpong][0]->bind(GL_TEXTURE6);
	indirect[!pingpong][1]->bind(GL_TEXTURE7);
	indirect[!pingpong][2]->bind(GL_TEXTURE8);

	_secLightProgram->setSampler("lightPos", 10);
	_secLightProgram->setSampler("lightDir", 11);
	_secLightProgram->setSampler("lightColor", 12);
	_indirectLights[0]->bind(GL_TEXTURE10);
	_indirectLights[1]->bind(GL_TEXTURE11);
	_indirectLights[2]->bind(GL_TEXTURE12);
	Vec4 c;
	GLuint startX=0, startY=0;
	GLuint width=0, height=0;
	bool started = false;
	int count=0;
	int total=0;
	for (GLuint x=0; x<_size[0]/DIS_REDUCTION; x++) {
		for (GLuint y=0; y<_size[1]/DIS_REDUCTION; y++) {
			total++;
			c = _disBuffer[DIS_INDEX]->getPixel(Vec2s(x,y));
			// Try to group vertical (or horizontal depending on the loop above) pixels to form one quad
			if (c[0] > 0.0) count++;
			if (c[0] > 0.0 && !started) {
				started = true;
				startX = ((x == 0) ? x : (x+1))*DIS_REDUCTION;// + ((x == 0) ? 0 : DIS_REDUCTION/2);
				startY = ((y == 0) ? y : (y+1))*DIS_REDUCTION;// + ((y == 0) ? 0 : DIS_REDUCTION/2);
				width = ((x == 0) ? 2 : 1)*DIS_REDUCTION;// + ((x == 0) ? DIS_REDUCTION/2 : 0);
				height = ((y == 0) ? 2 : 1)*DIS_REDUCTION;// + ((y == 0) ? DIS_REDUCTION/2 : 0);
			} else {
				height += DIS_REDUCTION;
			}
			if ((c[0] == 0.0 && started) || (started && y+1 == _size[1]/DIS_REDUCTION)) {
				started = false;
				drawQuad(startX, startY, width, height);
			}
		}
	}
	static bool firstTime = true;
	if (firstTime)
		std::cerr << (float)count/(float)total*100 <<  "%" << std::endl;
	firstTime = false;
	

	_fboIndirect->unbind();
	_secLightProgram->unload();

	return !pingpong;

}

/////////////////////////////////////////////////

void Engine::renderFinal(const Camera& cam)
{
	// Next, render from the camera with the main light on
	// TODO better scales?
	int pingpong=0;
	int old_pingpong=0;
	unsigned int offset=0;
    float neg_scale = 3.0;

	if (_radOn != DIRECT_ONLY) {

		for (GLuint i=0; i<_numSecLights/LIGHTS_PER_PASS; i++) {
			pingpong = renderIndirect(cam, &_ds[LO_RES_INDEX][0], _indirect[LO_RES_INDEX],
                                      offset, pingpong, i ? 1 : 0);
			offset += LIGHTS_PER_PASS;
		}

		for (GLuint i=0; i<3; i++) {
            _indirect[LO_RES_INDEX][!pingpong][i]->drawFullScreen_begin();
            _fboUpSample->bind();
            _fboUpSample->drawColorAttachments(1);
            _fboUpSample->attachTexture(_indirect[0][((_numSecLights/LIGHTS_PER_PASS) + 1)%2][i],
                                        GL_COLOR_ATTACHMENT0_EXT);
            _indirect[LO_RES_INDEX][!pingpong][i]->drawFullScreen_end();
            _fboUpSample->unbind();
        }

		offset = 0;
		pingpong = 0;

		for (GLuint i=0; i<_numSecLights/LIGHTS_PER_PASS; i++) {
			pingpong = renderHiIndirect(cam, &_ds[0][0], _indirect[0], offset, pingpong, i ? 1: 0);
			offset += LIGHTS_PER_PASS;
		}
        old_pingpong = !pingpong;
        offset = 0;
        pingpong = 0;// _numNegLights%LIGHTS_PER_PASS ? 1 : 0;


        if (_useNegativeIndirectLight) {
            for (GLuint i=0; i<_numNegLights/LIGHTS_PER_PASS; i++) {
                pingpong = renderNegIndirect(cam, &_ds[LO_RES_INDEX][0],
                                             _negIndirect[LO_RES_INDEX],
                                             _indirect[0][old_pingpong],
                                             offset, pingpong, i ? 1 : 0, neg_scale);
                offset += LIGHTS_PER_PASS;
            }

            _negIndirect[LO_RES_INDEX][!pingpong]->drawFullScreen_begin();
            _fboUpSample->bind();
            _fboUpSample->drawColorAttachments(1);
            //_fboUpSample->attachTexture(_negIndirect[0][1], GL_COLOR_ATTACHMENT0_EXT);
            _fboUpSample->attachTexture(_negIndirect[0][((_numNegLights/LIGHTS_PER_PASS) + 1)%2], GL_COLOR_ATTACHMENT0_EXT);
            _negIndirect[LO_RES_INDEX][!pingpong]->drawFullScreen_end();
            _fboUpSample->unbind();

            offset = 0;
            pingpong = 0;

            for (GLuint i=0; i<_numNegLights/LIGHTS_PER_PASS; i++) {
                pingpong = renderHiNegIndirect(cam, &_ds[0][0], 
                                               _negIndirect[0], 
                                               _indirect[0][old_pingpong],
                                               offset, pingpong, i ? 1: 0, neg_scale);
                offset += LIGHTS_PER_PASS;
            }
        }
	} 

	pingpong = !pingpong;

	if (_radOn == INDIRECT_ONLY) {
		//_indirect[_displayTex][pingpong][0]->drawFullScreen();
		_indirect[0][old_pingpong][0]->drawFullScreen();
		//_indirect[0][!old_pingpong][1]->drawFullScreen();
	} else if (_radOn == NEGATIVE_ONLY) {
		_negIndirect[0][pingpong]->drawFullScreen();
	} else {
		_defaultProgram->load();
		_defaultProgram->setFloat("lightScale", 1.0);
		_defaultProgram->setInt("useNegativeIndirectLight", _useNegativeIndirectLight);
		_defaultProgram->setFloat("indirectLookupScale", 1.0/pow(2, LO_RES_INDEX));
		_defaultProgram->setSampler("indirectTex", 6);
		_defaultProgram->setSampler("indirectNegTex", 7);
		
		_defaultProgram->setFloat("indirectScale", _radOn == DIRECT_ONLY ? 0.0 : 1.0);
		_ds[0][0]->bind(GL_TEXTURE0);
		_ds[0][1]->bind(GL_TEXTURE1);
		_ds[0][2]->bind(GL_TEXTURE5);
		_indirect[0][old_pingpong][0]->bind(GL_TEXTURE6);
		_negIndirect[0][pingpong]->bind(GL_TEXTURE7);
		drawQuad(0,0,_size[0], _size[1]);
		_fboIndirect->unbind();
		_defaultProgram->unload();
	}

	// Draw the light...huge hack
	//glDisable(GL_DEPTH_TEST);
	//drawLight(_scene->getLight(0), cam);

}

/////////////////////////////////////////////////

void Engine::drawEntities(LightPtr l, const Camera& c)
{
	assert(_scene != 0);
	assert(l != 0);

	Vec2s size = c.getSize();
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(c.getFovy(), c.getAspectRatio(), c.getNear(), c.getFar());
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	Vec3 pos = c.getPos();
	Vec3 at = c.getAt();
	Vec3 up = c.getUp();
	gluLookAt(pos[0], pos[1], pos[2], at[0], at[1], at[2], up[0], up[1], up[2]); 
	glEnable(GL_LIGHT0);
	pos = l->getCenter();
	Vec3 dir = l->getDir();
	Vec3 att = l->getAtt();
	GLfloat lPos[] = { pos[0], pos[1], pos[2], 1.0};
	GLfloat lDir[] = { dir[0], dir[1], dir[2], 1.0};
	glLightfv(GL_LIGHT0, GL_POSITION, lPos);
	glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, lDir);
	glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, l->getFov());
	glLightf(GL_LIGHT0, GL_SPOT_EXPONENT, l->getSpotExp());
	glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, att[0]);
	glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, att[1]);
	glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, att[2]);
 	for (unsigned int i=0; i<_scene->numEntities(); ++i) {
		EntityPtr e = _scene->getEntity(i);
		ModelPtr m = e->getModel();
		Vec3 p = e->getPosition();
		float angle = e->getRotAngle();
		Vec3 axis = e->getRotAxis();
		Vec3 s = e->getScale();
		glPushMatrix();
		glTranslatef(p[0], p[1], p[2]);
		glRotatef(angle, axis[0], axis[1], axis[2]);
		glScalef(s[0], s[1], s[2]);

        if (m->hasBoundingBox()) {
            GLfloat modelview_matrix[16];
            GLfloat projection_matrix[16];
            Vec4 projBBCorners[8];
            glGetFloatv(GL_MODELVIEW_MATRIX, modelview_matrix); 
            glGetFloatv(GL_PROJECTION_MATRIX, projection_matrix); 

            for (int j=0; j<8; j++) {
                projBBCorners[j] = 
                    MxV(projection_matrix, MxV(modelview_matrix, m->bbCorners[j])); 

                projBBCorners[j] /= projBBCorners[j][3];
                if (j==0) {
                    _BBmax[0] = _BBmin[0] = projBBCorners[j][0];
                    _BBmax[1] = _BBmin[1] = projBBCorners[j][1];
                } else {
                    _BBmax[0] = (_BBmax[0] < projBBCorners[j][0]) ? projBBCorners[j][0] : _BBmax[0]; 
                    _BBmax[1] = (_BBmax[1] < projBBCorners[j][1]) ? projBBCorners[j][1] : _BBmax[1]; 
                    _BBmin[0] = (_BBmin[0] > projBBCorners[j][0]) ? projBBCorners[j][0] : _BBmin[0]; 
                    _BBmin[1] = (_BBmin[1] > projBBCorners[j][1]) ? projBBCorners[j][1] : _BBmin[1]; 
                }
            }

            for (int i=0;i<2;i++) {
                _BBmin[i] = (_BBmin[i]*0.5 + 0.5) * (float)_size[i];
                _BBmax[i] = (_BBmax[i]*0.5 + 0.5) * (float)_size[i];
            }

            standardSamples();

            if (m->drawBoundingBox()) {
                glBegin(GL_LINES);
                for (int i=0;i<8;i++) {
                    glVertex3f(m->bbCorners[i][0], m->bbCorners[i][1],m->bbCorners[i][2]);
                }
                glEnd();
                glBegin(GL_LINE_STRIP);
                for (int i=0;i<8;i+=2) {
                    glVertex3f(m->bbCorners[i][0], m->bbCorners[i][1],m->bbCorners[i][2]);
                }
                glVertex3f(m->bbCorners[0][0], m->bbCorners[0][1],m->bbCorners[0][2]);
                glEnd();
                glBegin(GL_LINE_STRIP);
                for (int i=1;i<8;i+=2) {
                    glVertex3f(m->bbCorners[i][0], m->bbCorners[i][1],m->bbCorners[i][2]);
                }
                glVertex3f(m->bbCorners[1][0], m->bbCorners[1][1],m->bbCorners[1][2]);
                glEnd();
            }
        }
		drawModel(m);
		glPopMatrix();
	}
	glDisable(GL_LIGHT0);
}

/////////////////////////////////////////////////

void Engine::drawModel(ModelPtr m)
{
	assert(m != 0);
	if (m->getDisplayList() == 0) {
		GLuint list = glGenLists(1);
		glNewList(list, GL_COMPILE);
		// std::cerr << "Generating display list for " << m->getName() << std::endl;
		for (unsigned int p=0; p<m->parts.size(); ++p) {
			PartPtr part(m->parts[p]);		
			glMaterialfv(GL_FRONT, GL_DIFFUSE, part->mat.diffuse.ptr());
			glBegin(GL_TRIANGLES);
			for (unsigned int i=0; i<part->tris.size(); ++i) {
				Triangle t = part->tris[i];
				Vec3 v0 = part->verts[t.v[0]];
				Vec3 v1 = part->verts[t.v[1]];
				Vec3 v2 = part->verts[t.v[2]];

				Vec3 n0 = part->norms[t.n[0]];
				Vec3 n1 = part->norms[t.n[1]];
				Vec3 n2 = part->norms[t.n[2]];

				glNormal3f(n0[0], n0[1], n0[2]);
				glVertex3f(v0[0], v0[1], v0[2]);

				glNormal3f(n1[0], n1[1], n1[2]);
				glVertex3f(v1[0], v1[1], v1[2]);

				glNormal3f(n2[0], n2[1], n2[2]);
				glVertex3f(v2[0], v2[1], v2[2]);
			}
			glEnd();
		}
		glEndList();
		m->setDisplayList(list);
	}
	glCallList(m->getDisplayList());
#if 0
	for (unsigned int p=0; p<m->parts.size(); ++p) {
		PartPtr part(m->parts[p]);		
		glMaterialfv(GL_FRONT, GL_DIFFUSE, part->mat.diffuse.ptr());
        if (part->vbo)
        {

            /*
            // Vertex Buffer with indexed elements
            glBindBuffer(GL_ARRAY_BUFFER, part->array_buffer_name);
            glNormalPointer( GL_FLOAT, 6*sizeof(GLfloat), BUFFER_OFFSET(sizeof(GLfloat)*3));
            glVertexPointer(3, GL_FLOAT, 6*sizeof(GLfloat), 0);
            glEnableClientState(GL_NORMAL_ARRAY);
            glEnableClientState(GL_VERTEX_ARRAY);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, part->element_buffer_name);
            glDrawElements(GL_TRIANGLES, part->tris.size()*3, GL_UNSIGNED_INT, 0);
            */

            ///*
            // Vertex Buffer
            glBindBuffer(GL_ARRAY_BUFFER, part->vbo);
            glNormalPointer( GL_FLOAT, 6*sizeof(GLfloat), BUFFER_OFFSET(sizeof(GLfloat)*3));
            glVertexPointer(3, GL_FLOAT, 6*sizeof(GLfloat), 0);
            glEnableClientState(GL_NORMAL_ARRAY);
            glEnableClientState(GL_VERTEX_ARRAY);
            glDrawArrays(GL_TRIANGLES, 0, (int)part->tris.size()*3);
            glDisableClientState(GL_VERTEX_ARRAY);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            //*/

        }
        else
        {
            glBegin(GL_TRIANGLES);
            for (unsigned int i=0; i<part->tris.size(); ++i) {
                Triangle t = part->tris[i];
                Vec3 v0 = part->verts[t.v[0]];
                Vec3 v1 = part->verts[t.v[1]];
                Vec3 v2 = part->verts[t.v[2]];

                Vec3 n0 = part->norms[t.n[0]];
                Vec3 n1 = part->norms[t.n[1]];
                Vec3 n2 = part->norms[t.n[2]];
                
                glNormal3f(n0[0], n0[1], n0[2]);
                glVertex3f(v0[0], v0[1], v0[2]);
                
                glNormal3f(n1[0], n1[1], n1[2]);
                glVertex3f(v1[0], v1[1], v1[2]);

                glNormal3f(n2[0], n2[1], n2[2]);
                glVertex3f(v2[0], v2[1], v2[2]);
            }
            glEnd();
        }
	}
#endif
}

/////////////////////////////////////////////////

void Engine::drawLight(LightPtr l, const Camera& c)
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(c.getFovy(), c.getAspectRatio(), c.getNear(), c.getFar());
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	Vec3 pos = c.getPos();
	Vec3 at = c.getAt();
	Vec3 up = c.getUp();
	gluLookAt(pos[0], pos[1], pos[2], at[0], at[1], at[2], up[0], up[1], up[2]); 

	assert(l != 0);
	Vec3 dif = l->getDiffuse();
	Vec3 v0 = l->getVert(0);
	Vec3 v1 = l->getVert(1);
	Vec3 v2 = l->getVert(2);
	Vec3 v3 = l->getVert(3);
	glColor3f(dif[0], dif[1], dif[2]); 
	glBegin(GL_TRIANGLES);
	glVertex3f(v0[0], v0[1], v0[2]);
	glVertex3f(v1[0], v1[1], v1[2]);
	glVertex3f(v2[0], v2[1], v2[2]);

	glVertex3f(v2[0], v2[1], v2[2]);
	glVertex3f(v3[0], v3[1], v3[2]);
	glVertex3f(v0[0], v0[1], v0[2]);
	glEnd();
}

/////////////////////////////////////////////////
void Engine::drawQuad(int x, int y, int width, int height)
{
	glBegin(GL_QUADS);
	// When using TEXTURE_RECTANGLE_NV, tex coords are not normalized 
	glTexCoord2i(x, y);
	GLfloat fx = (float)x/(float)_size[0]*2 - 1.0;
	GLfloat fy = (float)y/(float)_size[1]*2 - 1.0;
	GLfloat fw = (float)width/(float)_size[0]*2;
	GLfloat fh = (float)height/(float)_size[1]*2;

	glVertex3f(fx, fy, 0.0);
	
	glVertex3f(fx+fw, fy, 0.0);
	
	glVertex3f(fx+fw, fy+fh, 0.0);
	
	glVertex3f(fx, fy+fh, 0.0);
	glEnd();
	checkGLError();
	// TODO just use one component
}


